﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using GojiSoft.EventSequenceUnitTesting.ExampleSupport;

namespace GojiSoft.EventSequenceUnitTesting {
    public class EventMonitor : IDisposable {
        #region Constants

        protected const int ZeroTimeoutMS = 0;

        #endregion

        #region Instance Variables

        private readonly object _syncObj = new object();
        private readonly AutoResetEvent _eventRaisedHandle = new AutoResetEvent(false);

        private readonly object _eventPublisher;
        private readonly int _eventTimeoutMS;
        private readonly Dictionary<string, Delegate> _eventHandlerMap = new Dictionary<string, Delegate>();
        private readonly List<string> _expectedEventSequence;
        private string[] _actualEventSequence = new string[100];
        private volatile int _eventCounter;

        #endregion

        #region Constructors

        protected EventMonitor(object eventPublisher, IEnumerable<string> expectedEventSequence, int eventTimeoutMS) {
            if(eventPublisher == null)
                throw new ArgumentNullException("eventPublisher");
            if(eventTimeoutMS < 0)
                throw new ArgumentOutOfRangeException("eventTimeoutMS", "Argument value must be zero or greater.");
            if(expectedEventSequence == null)
                throw new ArgumentNullException("expectedEventSequence");

            _eventPublisher = eventPublisher;
            _eventTimeoutMS = eventTimeoutMS;
            _expectedEventSequence = new List<string>(expectedEventSequence);
        }

        #endregion

        #region Public Methods

        public static void Assert(Action test, object eventPublisher, IEnumerable<string> expectedEventSequence) {
            Assert(test, eventPublisher, expectedEventSequence, ZeroTimeoutMS);
        }

        public static void Assert(Action test, object eventPublisher, IEnumerable<string> expectedEventSequence, int eventTimeoutMS) {
            using(var monitor = new EventMonitor(eventPublisher, expectedEventSequence, eventTimeoutMS)) {
                monitor.StartTest(test);
            }
        }

        #endregion

        #region Protected Methods

        protected virtual void StartTest(Action test) {
            if(test == null)
                throw new ArgumentNullException("test");

            try {
                SubscribeToEvents();

                test();

                MonitorExpectedEventSequence();

                AssertExpectations();
            } finally {
                UnsubscribeFromEvents();
            }
        }

        #endregion

        #region Private Methods

        private void SubscribeToEvents() {
            foreach(EventInfo eventInfo in _eventPublisher.GetType().GetEvents()) {
                Delegate proxyEventHandler = DynamicEvent.CreateProxyEventHandler(eventInfo, _eventPublisher, EventHandler);

                eventInfo.GetAddMethod().Invoke(_eventPublisher, new[] { proxyEventHandler });

                _eventHandlerMap.Add(eventInfo.Name, proxyEventHandler);
            }
        }

        private void EventHandler(object sender, EventArgs e, string eventName) {
#pragma warning disable 420 //(TL) Argument by ref of a volatile field is valid with the Interlocked API.
            int eventIndex = Interlocked.Increment(ref _eventCounter);
#pragma warning restore 420

            PropertyChangedEventArgs propertyChangedEventArgs = (e as PropertyChangedEventArgs);

            if(propertyChangedEventArgs != null) {
                eventName = propertyChangedEventArgs.PropertyName;
            }

            RecordActualEvent(eventIndex, eventName);

            _eventRaisedHandle.Set();
        }

        private void MonitorExpectedEventSequence() {
            int expectedEventCount = _expectedEventSequence.Count;

            if(expectedEventCount == 0) {
                Synchronization.Wait(_eventRaisedHandle, _eventTimeoutMS);
            } else {
                while(_eventCounter != expectedEventCount) {
                    if(!Synchronization.Wait(_eventRaisedHandle, _eventTimeoutMS)) {
                        string message = string.Format(CultureInfo.InvariantCulture,
                            "Event sequence monitoring timed out after waiting {0}ms for an event.{1}",
                            _eventTimeoutMS, GetEventSequenceReport());

                        if(_eventTimeoutMS == ZeroTimeoutMS) {
                            message += Environment.NewLine + "Event timeout was set to the default value of 0ms. For asynchronous events, increase this value when calling Assert.";
                        }

                        throw new EventMonitoringException(message);
                    }
                }
            }
        }

        private void UnsubscribeFromEvents() {
            foreach(EventInfo eventInfo in _eventPublisher.GetType().GetEvents()) {
                Delegate proxyEventHandler;

                if(_eventHandlerMap.TryGetValue(eventInfo.Name, out proxyEventHandler)) {
                    eventInfo.GetRemoveMethod().Invoke(_eventPublisher, new[] { proxyEventHandler });
                }
            }
        }

        private void RecordActualEvent(int eventIndex, string eventName) {
            if(eventIndex <= _actualEventSequence.Length) {
                _actualEventSequence[eventIndex - 1] = eventName;
            } else {
                lock(_syncObj) {
                    if(eventIndex > _actualEventSequence.Length) {
                        int newLength = (_actualEventSequence.Length * 2);

                        string[] newSequence = new string[newLength];

                        Array.Copy(_actualEventSequence, newSequence, _actualEventSequence.Length);

                        _actualEventSequence = newSequence;
                    }

                    _actualEventSequence[eventIndex - 1] = eventName;
                }
            }
        }

        private void AssertExpectations() {
            if(!_expectedEventSequence.SequenceEqual(_actualEventSequence.Take(_eventCounter))) {
                throw new EventMonitoringException("Unexpected event sequence." + GetEventSequenceReport());
            }
        }

        private string GetEventSequenceReport() {
            string report = string.Format(CultureInfo.InvariantCulture,
                @"{0} Expected sequence:{1}.{0} Actual sequence  :{2}.",
                Environment.NewLine,
                GetEventSequenceString(_expectedEventSequence, _expectedEventSequence.Count),
                GetEventSequenceString(_actualEventSequence, _eventCounter));

            return report;
        }

        private static string GetEventSequenceString(IList<string> sequence, int count) {
            StringBuilder sb = new StringBuilder(sequence.Count != 0 ? "(start)" : "(no events)");

            for(int i = 0; i < count; i++) {
                sb.Append(" (" + (i + 1) + ")" + sequence[i]);
            }

            return sb.ToString();
        }

        #endregion

        #region IDisposable : Members

        public void Dispose() {
            this.Dispose(true);

            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool isDisposing) {
            if(isDisposing) {
                if(_eventRaisedHandle != null) {
                    ((IDisposable)_eventRaisedHandle).Dispose();
                }
            }
        }

        #endregion
    }
}